<?php

namespace Xin\Coze;

use Carbon\Carbon;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Psr\SimpleCache\CacheInterface;
use Psr\SimpleCache\InvalidArgumentException;
use Xin\Support\Arr;
use Xin\Support\Json;
use Xin\Support\Time;

class AccessToken
{
	/**
	 * @var string
	 */
	protected string $appId;

	/**
	 * @var string
	 */
	protected $publicKeyId;

	/**
	 * @var string
	 */
	protected string $privateKey;

	/**
	 * @var int
	 */
	protected $expiredTime;

	/**
	 * @var CacheInterface
	 */
	protected $cache;

	/**
	 * @var callable
	 */
	protected static $cacheResolver;

	/**
	 * @var array
	 */
	protected $config;

	/**
	 * @var string
	 */
	protected $memoryTokens = [];

	/**
	 * 构造函数
	 * @param string $appId
	 * @param string $publicKeyId
	 * @param string $privateKey
	 * @param int|null $expiredTime
	 * @param array $config
	 */
	public function __construct(string $appId, string $publicKeyId, string $privateKey, int $expiredTime = null, array $config = [])
	{
		$this->appId = $appId;
		$this->publicKeyId = $publicKeyId;
		$this->privateKey = $this->loadPrivateKey($privateKey);
		$this->expiredTime = $expiredTime ?: 86399;
		$this->config = array_replace_recursive([
			// "cache_ttl" => $expiredTime - 60,
			'cache_prefix' => '',
		], $config);
	}

	/**
	 * 加载私钥
	 * @param string $privateKey
	 * @return string
	 */
	protected function loadPrivateKey(string $privateKey)
	{
		if (file_exists($privateKey)) {
			$privateKey = file_get_contents($privateKey);
		}

		return $privateKey;
	}

	/**
	 * 生成头部
	 * @return array
	 */
	protected function buildHeader()
	{
		return [
			"alg" => "RS256",          // 固定为RS256
			"typ" => "JWT",            // 固定为 JWT
			"kid" => $this->publicKeyId, // OAuth 应用的公钥指纹
		];
	}

	/**
	 * 生成头部 Base64
	 * @return string
	 */
	protected function buildHeaderAsBase64Url()
	{
		return self::base64UrlEncode(json_encode($this->buildHeader()));
	}

	/**
	 * 生成负载
	 * @return array
	 * @noinspection PhpDocMissingThrowsInspection
	 */
	protected function buildPayload($sessionName = null)
	{
		return [
			"iss" => $this->appId,   // OAuth 应用的 ID
			"aud" => "api.coze.cn",   //扣子 API 的Endpoint
			"iat" => now()->getTimestamp(),       // JWT开始生效的时间，秒级时间戳
			"exp" => $this->makeExpiredTime()->getTimestamp(),       // JWT过期时间，秒级时间戳
			"jti" => md5(random_bytes(32)), // 随机字符串，防止重放攻击
			// "session_name" => $sessionName,
		];
	}

	/**
	 * 生成负载 Base64
	 * @return string
	 */
	protected function buildPayloadAsBase64Url($sessionName = null)
	{
		return self::base64UrlEncode(json_encode($this->buildPayload($sessionName)));
	}

	/**
	 * 生成签名
	 * @return string
	 */
	protected function makeSignature($headerAsBase64Url, $payloadAsBase64Url)
	{

		return self::signRS256($headerAsBase64Url . "." . $payloadAsBase64Url, $this->privateKey);
	}

	/**
	 * 生成过期时间
	 * @return Carbon|\DateTime
	 */
	protected function makeExpiredTime()
	{
		return now()->addSeconds($this->expiredTime);
	}

	/**
	 * 获取 Token
	 * @param string|null $sessionName
	 * @param bool $refresh
	 * @return array
	 * @throws GuzzleException
	 * @throws InvalidArgumentException
	 */
	public function token(string $sessionName = null, bool $refresh = false)
	{
		return $this->getOnMemory(function () use ($sessionName) {
			// 生成 JWT Token
			$headerAsBase64Url = $this->buildHeaderAsBase64Url();
			$payloadAsBase64Url = $this->buildPayloadAsBase64Url($sessionName);
			$signature = $this->makeSignature($headerAsBase64Url, $payloadAsBase64Url);
			$jwtToken = $headerAsBase64Url . "." . $payloadAsBase64Url . "." . $signature;
			return $this->oauth2Token($jwtToken);
		}, $sessionName, $refresh);
	}

	/**
	 * 从内存、缓存中获取 Token
	 * @param callable $callback
	 * @param string|null $sessionName
	 * @param bool $refresh
	 * @return mixed
	 * @throws InvalidArgumentException
	 */
	protected function getOnMemory(callable $callback, string $sessionName = null, bool $refresh = false)
	{
		$cacheKey = $this->getCacheKey($sessionName);

		if (!$refresh) {
			// 从内存中获取
			$token = $this->memoryTokens[$sessionName] ?? null;
			// 验证 Token 是否有效
			if ($token && $token['expires_in'] > Time::nowTimestamp()) {
				// 重新缓存 Token，这样可以让 Token 在数组的最后一项，代表一直在用
				$this->appendMemoryTokenWithClear($token, $sessionName);
				return $token;
			}

			// 从缓存中获取
			$token = $this->cache()->get($cacheKey);
			if ($token) {
				// 保存到内存中
				$this->appendMemoryTokenWithClear($token, $sessionName);
				return $token;
			}
		}

		// 生成 Token
		$token = $callback();

		// 保存到内存中
		$this->appendMemoryTokenWithClear($token, $sessionName);

		// 缓存 Token
		$this->cache()->set($cacheKey, $token, $this->expiredTime - 60);

		return $token;
	}

	/**
	 * 清理内存中的 Token，始终保持 最多 n 个 Token
	 */
	protected function clearMemoryTokens()
	{
		$maxMemoryTokens = Arr::get($this->config, 'max_memory_tokens', 10);
		$currentMemoryTokens = count($this->memoryTokens);
		if ($currentMemoryTokens > $maxMemoryTokens) {
			for ($i = 0; $i < $currentMemoryTokens - $maxMemoryTokens; $i++) {
				array_shift($this->memoryTokens);
			}
		}
	}

	/**
	 * 追加内存中的 Token，并清理多余的 Token
	 * @param array $token
	 * @param string|null $sessionName
	 */
	protected function appendMemoryTokenWithClear(array $token, string $sessionName = null)
	{
		$this->memoryTokens[$sessionName] = $token;
		$this->clearMemoryTokens();
	}

	/**
	 * @param string $jwtToken
	 * @return array
	 * @throws GuzzleException
	 */
	protected function oauth2Token($jwtToken)
	{
		$client = new Client([
			'base_uri' => 'https://api.coze.cn/api/',
			'headers' => [
				'Authorization' => 'Bearer ' . $jwtToken,
				'Content-Type' => 'application/json',
				'Accept' => 'application/json',
			],
		]);

		$response = $client->post('permission/oauth2/token', [
			'json' => [
				'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
				'duration_seconds' => $this->expiredTime,
			],
		]);

		return Json::array($response->getBody()->getContents());
	}

	/**
	 * 使用 RS256 签名
	 * @param string $data
	 * @param string $privateKey
	 * @return string
	 */
	public static function signRS256($data, $privateKey)
	{
		$privateKeyResource = openssl_get_privatekey($privateKey);
		if (!$privateKeyResource) {
			throw new \LogicException("无法加载私钥");
		}

		$signature = '';
		$result = openssl_sign($data, $signature, $privateKeyResource, OPENSSL_ALGO_SHA256);

		// PHP 8.0 之后，openssl_free_key() 已经被废弃
		if (PHP_MAJOR_VERSION < 8) {
			openssl_free_key($privateKeyResource);
		}

		if (!$result) {
			throw new \LogicException("签名失败");
		}

		// return $signature;
		return self::base64urlEncode($signature);
	}

	/**
	 * 验证 RS256 签名
	 * @param string $data
	 * @param string $signature
	 * @param string $publicKey
	 * @return bool
	 */
	public static function verifyRS256($data, $signature, $publicKey)
	{
		$publicKeyResource = openssl_get_publickey($publicKey);
		if (!$publicKeyResource) {
			throw new \LogicException("无法加载公钥");
		}

		$signature = self::base64UrlDecode($signature);
		$result = openssl_verify($data, $signature, $publicKeyResource, OPENSSL_ALGO_SHA256);

		// PHP 8.0 之后，openssl_free_key() 已经被废弃
		if (PHP_MAJOR_VERSION < 8) {
			openssl_free_key($publicKeyResource);
		}

		if (!$result) {
			throw new \LogicException("解签失败");
		}

		return $result === 1;
	}

	/**
	 * Base64 URL 编码
	 * @param string $data
	 * @return string
	 */
	protected static function base64UrlEncode($data)
	{
		return rtrim(str_replace(['+', '/'], ['-', '_'], base64_encode($data)), '=');
	}

	/**
	 * Base64 URL 解码
	 * @param string $data
	 * @return string
	 */
	protected static function base64UrlDecode($data)
	{
		return base64_decode(str_replace(['-', '_'], ['+', '/'], $data) . '===');
	}

	/**
	 * 获取缓存解析器
	 * @return callable
	 */
	public static function getCacheResolver()
	{
		return self::$cacheResolver;
	}

	/**
	 * 设置缓存解析器
	 * @param callable $cacheResolver
	 * @return void
	 */
	public static function setCacheResolver(callable $cacheResolver)
	{
		self::$cacheResolver = $cacheResolver;
	}

	/**
	 * 获取缓存
	 * @return CacheInterface
	 */
	protected function cache()
	{
		if ($this->cache === null) {
			if (!is_callable(self::$cacheResolver)) {
				throw new \LogicException("[" . static::class . "] 缓存解析器未设置");
			}

			$cache = call_user_func(self::$cacheResolver);
			if (!($cache instanceof CacheInterface)) {
				throw new \LogicException("[" . static::class . "] 缓存解析器返回值错误，必须是 " . CacheInterface::class);
			}

			$this->cache = $cache;
		}

		return $this->cache;
	}

	/**
	 * 设置缓存
	 * @param CacheInterface $cache
	 * @return void
	 */
	public function setCache(CacheInterface $cache)
	{
		$this->cache = $cache;
	}

	/**
	 * 获取缓存 Key
	 * @param string|null $sessionName
	 * @return string
	 */
	protected function getCacheKey(string $sessionName = null)
	{
		return $this->config['cache_prefix'] . $this->appId . "_" . ($sessionName ?? '');
	}
}
